package jdesktop.appbar;

import java.beans.PropertyChangeListener;

/**
 * Similar to insets, but uses a boolean flag for each side.
 * This is used to represent which Edges are legal, and which insets should be hidden.
 */
public class Sides implements PropertyChangeSource
{
    static public final String LEFT = "left";
    static public final String RIGHT = "right";
    static public final String TOP = "top";
    static public final String BOTTOM = "bottom";

    private boolean left, top, right, bottom;
    private final PropertyChangeSourceSupport pcs;

    /**
     * {@code true} if this instance is not mutable. To get a mutable instance
     * from a read-only instance, use {@link }Sides(Sides)}
     */
    private boolean readOnly;

    public Sides()
    {
        this(true);
    }

    public Sides(Sides copy)
    {
        this(copy.isBound());
        set(copy.isLeft(), copy.isTop(), copy.isRight(), copy.isBottom());
        // specifically, do not set read only.
    }


    public Sides(boolean bound)
    {
        if (bound)
            pcs = new PropertyChangeSourceSupport(this, false);
        else
            pcs = null; // maybe should use a no-op shared instance
    }

    public Sides(boolean left, boolean top, boolean right, boolean bottom)
    {
        this(true);
        this.left = left;
        this.top = top;
        this.right = right;
        this.bottom = bottom;
    }

    public void addPropertyChangeListener(PropertyChangeListener listener)
    {
        checkMutable();
        if (isBound())
            pcs.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener)
    {
        // we allow removal even when immutable
        if (isBound())
            pcs.removePropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener)
    {
        if (isBound())
            pcs.removePropertyChangeListener(propertyName, listener);
    }

    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener)
    {
        checkMutable();
        if (isBound())
            pcs.addPropertyChangeListener(propertyName, listener);
    }


    public boolean isLeft()
    {
        return left;
    }

    public void setLeft(boolean left)
    {
        checkMutable();
        boolean old = this.left;
        this.left = left;
        notifyChange(LEFT, old, left);
    }

    public boolean isTop()
    {
        return top;
    }

    public void setTop(boolean top)
    {
        checkMutable();
        boolean old = this.top;
        this.top = top;
        notifyChange(TOP, old, top);
    }

    public boolean isRight()
    {
        return right;
    }

    public void setRight(boolean right)
    {
        checkMutable();
        boolean old = this.right;
        this.right = right;
        notifyChange(RIGHT, old, right);
    }

    public boolean isBottom()
    {
        return bottom;
    }

    public void setBottom(boolean bottom)
    {
        checkMutable();
        boolean old = this.bottom;
        this.bottom = bottom;
        notifyChange(BOTTOM, old, bottom);
    }

    private void notifyChange(String property, boolean oldValue, boolean newValue)
    {
        if (oldValue!=newValue && isBound() && pcs.hasListeners(property))
            pcs.firePropertyChange(property, oldValue, newValue);
    }

    public boolean isSet(Edge e)
    {
        return getValueAt(e.getOrientation(), e.isOrigin());
    }

    public void set(Edge e, boolean value)
    {
        set(e.getOrientation(), e.isOrigin(), value);
    }

    public void set(Orientation o, boolean atOrigin, boolean value)
    {
        if (o.isHorizontal())
        {
            if (atOrigin)
                this.setLeft(value);
            else
                this.setRight(value);
        }
        else
        {
            if (atOrigin)
                this.setTop(value);
            else
                this.setBottom(value);
        }
    }

    public boolean getValueAt(Orientation o, boolean atOrigin)
    {
        boolean value;
        if (o.isHorizontal())
        {
            if (atOrigin)
                value = this.isLeft();
            else
                value = this.isRight();
        }
        else
        {
            if (atOrigin)
                value = this.isTop();
            else
                value = this.isBottom();
        }
        return value;
    }

    /**
     * Sets all values to the same.
     * @param value
     * @return {@code this} for convenience.
     */
    public Sides setAll(boolean value)
    {
        checkMutable();

        boolean old = setFreezeNotification(false);
        setLeft(value);
        setTop(value);
        setRight(value);
        setBottom(value);
        setFreezeNotification(old);
        return this;
    }

    /**
     * Computes in-place the logical {@code AND} of this side with an{@code other} side.
     * The result is stored in {@code this}.
     * @param other The other side - not modified.
     * @return {@code this}, which contains result of {@code AND}'ing {@code this} with {@code other}.
     */
    public Sides and(Sides other)
    {
        boolean old = setFreezeNotification(false);
        setLeft(isLeft() && other.isLeft());
        setTop(isTop() && other.isTop());
        setRight(isRight() && other.isRight());
        setBottom(isBottom() && other.isBottom());
        setFreezeNotification(old);
        return this;
    }

    /**
     * Computes in-place the logical {@code OR} of this side with an{@code other} side.
     * The result is stored in {@code this}.
     * @param other The other side - not modified.
     * @return {@code this}, which contains result of {@code OR}'ing {@code this} with {@code other}.
     */
    public Sides or(Sides other)
    {
        boolean old = setFreezeNotification(false);
        setLeft(isLeft() || other.isLeft());
        setTop(isTop() || other.isTop());
        setRight(isRight() || other.isRight());
        setBottom(isBottom() || other.isBottom());
        setFreezeNotification(old);
        return this;
    }

    public Sides not()
    {
        boolean old = setFreezeNotification(false);
        setLeft(!isLeft());
        setTop(!isTop());
        setRight(!isRight());
        setBottom(!isBottom());
        setFreezeNotification(old);
        return this;
    }

    public boolean setFreezeNotification(boolean freeze)
    {
        checkMutable();
        return (isBound()) ?
                pcs.setFreezeNotification(freeze) :
            false;
    }

    public boolean isBound()
    {
        return pcs!=null;
    }


    public Sides assign(Sides sides)
    {
        return setAll(false).or(sides);
    }

    /**
     * Determines if all edges are set to a value.
     * @param value The value to test against all edges.
     * @return {@code true} if all edges are set to {@code value}.
     */
    public boolean areAllSetTo(boolean value)
    {
        return isLeft()==value && isRight()==value && isTop()==value && isBottom()==value; 
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) return true;
        if (!(o instanceof Sides)) return false;

        Sides sides = (Sides) o;

        if (bottom != sides.bottom) return false;
        if (left != sides.left) return false;
        if (right != sides.right) return false;
        if (top != sides.top) return false;

        return true;
    }

    @Override
    public int hashCode()
    {
        int result = (left ? 1 : 0);
        result = 31 * result + (top ? 1 : 0);
        result = 31 * result + (right ? 1 : 0);
        result = 31 * result + (bottom ? 1 : 0);
        return result;
    }

    public void set(boolean left, boolean top, boolean right, boolean bottom)
    {        
        boolean old = setFreezeNotification(true);
        setLeft(left);
        setTop(top);
        setRight(right);
        setBottom(bottom);
        setFreezeNotification(old);
    }

    public boolean isReadOnly()
    {
        return readOnly;
    }

    /**
     * Sets the read only property. NB: this can only be set on a mutable instance,
     * so in practice the only state change is from mutable to read only.
     * @param readOnly The new value of read only.
     */
    public void setReadOnly(boolean readOnly)
    {
        boolean old = this.readOnly;
        if (old!=readOnly)
        {
            checkMutable();
            this.readOnly = readOnly;
        }
    }

    /**
     * Checks that this state is mutable. This should be called before any operation that
     * could change the state of this instance. (Note that it is also called even if the actual state
     * is not changing, but merely that it could potentially change. This is to make the behaviour consistent
     * and independent upon the property values.)
     */
    protected void checkMutable()
    {
        if (isReadOnly())
            throw new UnsupportedOperationException("instance is read-only");
    }
}
